/*-------------------------------------------------------------------------
 *
 * sinvaladt.c
 *	  POSTGRES 共享缓存失效数据管理器。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/ipc/sinvaladt.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <signal.h>
#include <unistd.h>

#include "access/transam.h"
#include "miscadmin.h"
#include "storage/backendid.h"
#include "storage/ipc.h"
#include "storage/proc.h"
#include "storage/procsignal.h"
#include "storage/shmem.h"
#include "storage/sinvaladt.h"
#include "storage/spin.h"


/*
 * 从概念上讲，共享缓存无效消息存储在一个无限数组中，其中 maxMsgNum 是下一个要存储已提交消息的数组下标，minMsgNum 是所有后端尚未读取的消息中最小的数组下标，并且我们始终有 maxMsgNum >= minMsgNum。（当没有待处理消息时，它们是相等的。）对于每个活动后端，有一个 nextMsgNum 指针指示其需要读取的下一条消息；对于每个后端，我们有 maxMsgNum >= nextMsgNum >= minMsgNum。
 *
 * （在当前实现中，minMsgNum 是每个进程的 nextMsgNum 值的下限，但它并不严格等于最小的 nextMsgNum——它可能滞后。我们仅在调用 SICleanupQueue 时更新它，并尝试不经常这样做。）
 *
 * 实际上，消息存储在 MAXNUMMESSAGES 条目的循环缓冲区中。我们通过计算 MsgNum % MAXNUMMESSAGES 将 MsgNum 值转换为循环缓冲区索引（只要 MAXNUMMESSAGES 是一个常数且是 2 的幂，这应该是快速的）。只要 maxMsgNum 不超过 minMsgNum 超过 MAXNUMMESSAGES，我们在缓冲区中就有足够的空间。如果缓冲区确实溢出，我们通过为每个落后过多的后端设置“重置”标志来恢复。处于“重置”状态的后端在确定 minMsgNum 时被忽略。当它最终尝试接收无效消息时，它必须丢弃其所有可失效状态，因为它不会知道自己错过了什么。
 *
 * 为了降低需要重置的概率，我们会向任何似乎落后得过远的后端发送“追赶”中断。正常情况下，一次最多只有一个这样的中断在传输中；当后端完成处理追赶中断时，它执行 SICleanupQueue，如果需要，将通知下一个落后得更远的后端。这避免了多个后端同时试图赶上而导致的不必要争用。然而，最落后的后端可能陷入了无法赶上的状态。最终，它将被重置，因此不会给自己以外的任何人带来更多问题。但我们也不想发现一大堆其他后端现在离重置阈值太近而无法被拯救。因此，SICleanupQueue 的设计是偶尔向排队变得更满的情况下，向那些落后且尚未得到的后端发送额外的追赶中断。只要没有很多“卡住”的后端，我们就不需要大量的额外中断，因为不被卡住的后端会将它们的中断传播给下一个。
 *
 * 如果 MsgNum 值溢出整数，我们会遇到问题，因此每当 minMsgNum 超过 MSGNUMWRAPAROUND 时，我们会同时从所有 MsgNum 变量中减去 MSGNUMWRAPAROUND。MSGNUMWRAPAROUND 可以很大，以便我们不需要经常这样做。它必须是 MAXNUMMESSAGES 的倍数，以便在我们这样做时不需要移动现有的循环缓冲区条目。
 *
 * 对共享 sinval 数组的访问受两个锁 SInvalReadLock 和 SInvalWriteLock 的保护。读取者以共享模式获取 SInvalReadLock；这允许他们修改自己的 ProcState，但不能修改或甚至查看其他人的。当我们需要执行数组范围更新时，例如在 SICleanupQueue 中，我们以独占模式获取 SInvalReadLock 以锁定所有读取者。写入者以独占模式获取 SInvalWriteLock，以序列化将消息添加到队列中。请注意，写入者可以与一个或多个读取者并行工作，因为写入者无需触及任何人的 ProcState，除非在不常见的情况下需要 SICleanupQueue。唯一的重叠点是，写入者希望更改 maxMsgNum，而读取者需要读取它。我们通过让读取者必须持有自旋锁足够长的时间以读取 maxMsgNum，而写入者只需持有足够长的时间以写入 maxMsgNum 来解决这个问题。（确切的规则是，如果您没有持有 SInvalWriteLock，则需要自旋锁才能读取 maxMsgNum，而除非您同时持有两个锁，否则需要自旋锁才能写入 maxMsgNum。）
 *
 * 注意：由于 maxMsgNum 是一个整数，因此可推测为原子可读/写，自旋锁可能显得不必要。需要它的原因是提供内存屏障：我们需要确保在 maxMsgNum 增加之前实际写入数组的消息确实存在，并且读取者在获取 maxMsgNum 后会看到该数据。具有弱内存顺序保证的多处理器如果没有自旋锁序列中包含的内存屏障指令，可能会出现失败。
 */



/****
 * 可配置的参数。
 *
 * MAXNUMMESSAGES：我们可以缓冲的共享无效消息的最大数量。
 * 必须是2的幂以提高速度。
 *
 * MSGNUMWRAPAROUND：为避免溢出而减少MsgNum变量的频率。
 * 必须是MAXNUMMESSAGES的倍数。应该很大。
 *
 * CLEANUP_MIN：在我们调用SICleanupQueue之前，缓冲区中必须存在的最低消息数量。
 *
 * CLEANUP_QUANTUM：一旦超过CLEANUP_MIN，调用SICleanupQueue的频率（以消息为单位）。
 * 应该是2的幂以提高速度。
 *
 * SIG_THRESHOLD：后端必须落后于的最低消息数量，
 * 才能向其发送PROCSIG_CATCHUP_INTERRUPT。
 *
 * WRITE_QUANTUM：每次SIInsertDataEntries迭代中推入缓冲区的最大消息数量。
 * 非关键，但应小于CLEANUP_QUANTUM，因为我们仅考虑在每次迭代中调用SICleanupQueue一次。
 */

#define MAXNUMMESSAGES 4096
#define MSGNUMWRAPAROUND (MAXNUMMESSAGES * 262144)
#define CLEANUP_MIN (MAXNUMMESSAGES / 2)
#define CLEANUP_QUANTUM (MAXNUMMESSAGES / 16)
#define SIG_THRESHOLD (MAXNUMMESSAGES / 2)
#define WRITE_QUANTUM 64

/* 每个后端在共享无效结构中的状态 */
typedef struct ProcState
{
	/* 在非活动ProcState数组项中procPid为零。 */
	pid_t		procPid;		/* 后端的PID，用于信号发送 */
	PGPROC	   *proc;			/* 后端的PGPROC */
	/* 如果procPid == 0或resetState为真，nextMsgNum则没有意义。 */
	int			nextMsgNum;		/* 下一个待读取的消息编号 */
	bool		resetState;		/* 后端需要重置其状态 */
	bool		signaled;		/* 后端已收到追赶信号 */
	bool		hasMessages;	/* 后端有未读消息 */

	/*
	 * 后端只发送失效信息，从不接收。 这只有在恢复期间的启动进程中才有意义，因为它不维护 relcache，然而它发送失效消息以允许查询后端看到模式更改。
	 */
	bool		sendOnly;		/* 后端只发送，永不接收 */

	/*
	 * 每个空闲后端插槽要使用的下一 LocalTransactionId。 我们将其保留在这里，因为它是按 BackendId 索引的，方便在设置 MyBackendId 时将值复制到本地内存中。 在活动的 ProcState 条目中它没有意义。
	 */
	LocalTransactionId nextLXID;
} ProcState;

/* 共享缓存失效内存段 */
typedef struct SISeg
{
	/*
	 * 一般状态信息
	 */
	int			minMsgNum;		/* 仍需的最旧消息 */
	int			maxMsgNum;		/* 下一个要分配的消息编号 */
	int			nextThreshold;	/* 要调用 SICleanupQueue 的消息数量 */
	int			lastBackend;	/* 最后一个活动 procState 条目的索引，加1 */
	int			maxBackends;	/* procState 数组的大小 */

	slock_t		msgnumLock;		/* 保护 maxMsgNum 的自旋锁 */

	/*
	 * 持有共享失效消息的循环缓冲区
	 */
	SharedInvalidationMessage buffer[MAXNUMMESSAGES];

	/*
	 * 每个后端失效状态信息（具有 MaxBackends 条目）。
	 */
	ProcState	procState[FLEXIBLE_ARRAY_MEMBER];
} SISeg;

static SISeg *shmInvalBuffer;	/* 指向共享失效缓冲区的指针 */


static LocalTransactionId nextLocalTransactionId;

static void fc_CleanupInvalidationState(int fc_status, Datum fc_arg);


/*
 * SInvalShmemSize --- 返回所需的共享内存空间
 */
Size SInvalShmemSize(void)
{
	Size		fc_size;

	fc_size = offsetof(SISeg, procState);

	/*
	 * 在热备模式下，启动进程使用 InitRecoveryTransactionEnvironment() 请求 procState 数组插槽。 尽管 MaxBackends 不考虑启动进程，但它保证会获得一个空闲插槽。 这是因为自动清理启动器和工作进程（包含在 MaxBackends 中）在热备模式下不会启动。
	 */
	fc_size = add_size(fc_size, mul_size(sizeof(ProcState), MaxBackends));

	return fc_size;
}

/*
 * CreateSharedInvalidationState
 *		创建并初始化 SI 消息缓冲区
 */
void CreateSharedInvalidationState(void)
{
	int			fc_i;
	bool		fc_found;

	/* 在共享内存中分配空间 */
	shmInvalBuffer = (SISeg *)
		ShmemInitStruct("shmInvalBuffer", SInvalShmemSize(), &fc_found);
	if (fc_found)
		return;

	/* 清除消息计数器，保存 procState 数组的大小，初始化自旋锁 */
	shmInvalBuffer->minMsgNum = 0;
	shmInvalBuffer->maxMsgNum = 0;
	shmInvalBuffer->nextThreshold = CLEANUP_MIN;
	shmInvalBuffer->lastBackend = 0;
	shmInvalBuffer->maxBackends = MaxBackends;
	SpinLockInit(&shmInvalBuffer->msgnumLock);

	/* buffer[] 数组最初全部未使用，因此我们不需要填充它 */

	/* 将所有后端标记为非活动，并初始化 nextLXID */
	for (fc_i = 0; fc_i < shmInvalBuffer->maxBackends; fc_i++)
	{
		shmInvalBuffer->procState[fc_i].procPid = 0;	/* inactive */
		shmInvalBuffer->procState[fc_i].proc = NULL;
		shmInvalBuffer->procState[fc_i].nextMsgNum = 0;	/* 毫无意义 */
		shmInvalBuffer->procState[fc_i].resetState = false;
		shmInvalBuffer->procState[fc_i].signaled = false;
		shmInvalBuffer->procState[fc_i].hasMessages = false;
		shmInvalBuffer->procState[fc_i].nextLXID = InvalidLocalTransactionId;
	}
}

/*
 * SharedInvalBackendInit
 *		初始化一个新后端以操作 sinval 缓冲区
 */
void SharedInvalBackendInit(bool fc_sendOnly)
{
	int			fc_index;
	ProcState  *fc_stateP = NULL;
	SISeg	   *fc_segP = shmInvalBuffer;

	/*
	 * 这可以与读取操作并行运行，但不能与写入操作并行，因为 SIInsertDataEntries 依赖 lastBackend 适当地设置 hasMessages。
	 */
	LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);

	/* 查找 procState 数组中的空闲条目 */
	for (fc_index = 0; fc_index < fc_segP->lastBackend; fc_index++)
	{
		if (fc_segP->procState[fc_index].procPid == 0)	/* 非活动插槽？ */
		{
			fc_stateP = &fc_segP->procState[fc_index];
			break;
		}
	}

	if (fc_stateP == NULL)
	{
		if (fc_segP->lastBackend < fc_segP->maxBackends)
		{
			fc_stateP = &fc_segP->procState[fc_segP->lastBackend];
			Assert(fc_stateP->procPid == 0);
			fc_segP->lastBackend++;
		}
		else
		{
			/*
			 * procState 插槽用尽：超出 MaxBackends -- 正常报告
			 */
			MyBackendId = InvalidBackendId;
			LWLockRelease(SInvalWriteLock);
			ereport(FATAL,
					(errcode(ERRCODE_TOO_MANY_CONNECTIONS),
					 errmsg("sorry, too many clients already")));
		}
	}

	MyBackendId = (fc_stateP - &fc_segP->procState[0]) + 1;

	/* 在 MyProc 中公布分配的后端 ID */
	MyProc->backendId = MyBackendId;

	/* 将下一个本地事务 ID 提取到本地内存中 */
	nextLocalTransactionId = fc_stateP->nextLXID;

	/* 标记自己为活动，所有现有消息均已读取 */
	fc_stateP->procPid = MyProcPid;
	fc_stateP->proc = MyProc;
	fc_stateP->nextMsgNum = fc_segP->maxMsgNum;
	fc_stateP->resetState = false;
	fc_stateP->signaled = false;
	fc_stateP->hasMessages = false;
	fc_stateP->sendOnly = fc_sendOnly;

	LWLockRelease(SInvalWriteLock);

	/* 注册退出例程以在退出时将我的条目标记为非活动 */
	on_shmem_exit(fc_CleanupInvalidationState, PointerGetDatum(fc_segP));

	elog(DEBUG4, "my backend ID is %d", MyBackendId);
}

/*
 * CleanupInvalidationState
 *		将当前后端标记为不再活动。
 *
 * 此函数在后端关闭期间通过 on_shmem_exit() 被调用。
 *
 * arg 实际上是 "SISeg*" 类型。
 */
static void fc_CleanupInvalidationState(int fc_status, Datum fc_arg)
{
	SISeg	   *fc_segP = (SISeg *) DatumGetPointer(fc_arg);
	ProcState  *fc_stateP;
	int			fc_i;

	Assert(PointerIsValid(fc_segP));

	LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);

	fc_stateP = &fc_segP->procState[MyBackendId - 1];

	/* 更新下一个本地事务 ID，以供下一个持有此 backendID 的使用 */
	fc_stateP->nextLXID = nextLocalTransactionId;

	/* 将我自己标记为不活动 */
	fc_stateP->procPid = 0;
	fc_stateP->proc = NULL;
	fc_stateP->nextMsgNum = 0;
	fc_stateP->resetState = false;
	fc_stateP->signaled = false;

	/* 重新计算最后一个活动后端的索引 */
	for (fc_i = fc_segP->lastBackend; fc_i > 0; fc_i--)
	{
		if (fc_segP->procState[fc_i - 1].procPid != 0)
			break;
	}
	fc_segP->lastBackend = fc_i;

	LWLockRelease(SInvalWriteLock);
}

/*
 * BackendIdGetProc
 *		根据后端 ID 获取 PGPROC 结构。
 *		结果可能会很快过时，因此调用者必须小心使用此信息。
 *		如果后端不活动，则返回 NULL。
 */
PGPROC * BackendIdGetProc(int fc_backendID)
{
	PGPROC	   *fc_result = NULL;
	SISeg	   *fc_segP = shmInvalBuffer;

	/* 需要锁定以防止后端的添加/移除 */
	LWLockAcquire(SInvalWriteLock, LW_SHARED);

	if (fc_backendID > 0 && fc_backendID <= fc_segP->lastBackend)
	{
		ProcState  *fc_stateP = &fc_segP->procState[fc_backendID - 1];

		fc_result = fc_stateP->proc;
	}

	LWLockRelease(SInvalWriteLock);

	return fc_result;
}

/*
 * BackendIdGetTransactionIds
 *		获取后端的 xid 和 xmin。结果可能会很快过时，
 *		因此调用者必须小心使用此信息。
 */
void BackendIdGetTransactionIds(int fc_backendID, TransactionId *fc_xid, TransactionId *fc_xmin)
{
	SISeg	   *fc_segP = shmInvalBuffer;

	*fc_xid = InvalidTransactionId;
	*fc_xmin = InvalidTransactionId;

	/* 需要锁定以防止后端的添加/移除 */
	LWLockAcquire(SInvalWriteLock, LW_SHARED);

	if (fc_backendID > 0 && fc_backendID <= fc_segP->lastBackend)
	{
		ProcState  *fc_stateP = &fc_segP->procState[fc_backendID - 1];
		PGPROC	   *fc_proc = fc_stateP->proc;

		if (fc_proc != NULL)
		{
			*fc_xid = fc_proc->xid;
			*fc_xmin = fc_proc->xmin;
		}
	}

	LWLockRelease(SInvalWriteLock);
}

/*
 * SIInsertDataEntries
 *		将新的失效消息添加到缓冲区。
 */
void SIInsertDataEntries(const SharedInvalidationMessage *fc_data, int fc_n)
{
	SISeg	   *fc_segP = shmInvalBuffer;

	/*
	 * N 可以是任意大的。我们将工作分成不超过
	 * WRITE_QUANTUM 消息的组，以确保我们不会长时间持有锁。
	 * （这并不是因为我们关心让其他写入者进入，而是因为
	 * 一些刚刚赶上的后端可能正在尝试执行 SICleanupQueue
	 * 来传递其信号，我们不希望它等待太长时间。）此外，
	 * 我们需要考虑定期调用 SICleanupQueue。
	 */
	while (fc_n > 0)
	{
		int			fc_nthistime = Min(fc_n, WRITE_QUANTUM);
		int			fc_numMsgs;
		int			fc_max;
		int			fc_i;

		fc_n -= fc_nthistime;

		LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);

		/*
		 * 如果缓冲区已满，我们 *必须* 获取一些空间。
		 * 清理队列并重置任何阻止空间释放的标记。
		 * 否则，仅在超过下一个满阈值时清理队列。
		 * 我们在任何调用 SICleanupQueue 后必须循环并重新检查缓冲区状态。
		 */
		for (;;)
		{
			fc_numMsgs = fc_segP->maxMsgNum - fc_segP->minMsgNum;
			if (fc_numMsgs + fc_nthistime > MAXNUMMESSAGES ||
				fc_numMsgs >= fc_segP->nextThreshold)
				SICleanupQueue(true, fc_nthistime);
			else
				break;
		}

		/*
		 * 将新消息插入循环缓冲区的适当位置
		 */
		fc_max = fc_segP->maxMsgNum;
		while (fc_nthistime-- > 0)
		{
			fc_segP->buffer[fc_max % MAXNUMMESSAGES] = *fc_data++;
			fc_max++;
		}

		/* 使用自旋锁更新 maxMsgNum 的当前值 */
		SpinLockAcquire(&fc_segP->msgnumLock);
		fc_segP->maxMsgNum = fc_max;
		SpinLockRelease(&fc_segP->msgnumLock);

		/*
		 * 现在 maxMsgNum 的更改在全局上是可见的，我们给每个人
		 * 一个快速的推动，以确保他们读取新添加的消息。
		 * 释放 SInvalWriteLock 将强制执行完整的内存屏障，
		 * 因此这些（未上锁的）更改将在我们退出函数之前
		 * 提交到内存。
		 */
		for (fc_i = 0; fc_i < fc_segP->lastBackend; fc_i++)
		{
			ProcState  *fc_stateP = &fc_segP->procState[fc_i];

			fc_stateP->hasMessages = true;
		}

		LWLockRelease(SInvalWriteLock);
	}
}

/*
 * SIGetDataEntries
 *		为当前后端获取下一个 SI 消息（如果有的话）
 *
 * 可能的返回值：
 *	0:	 没有可用的 SI 消息
 *	n>0: 下一个 n 个 SI 消息已提取到 data[]
 * -1:	 提取了 SI 重置消息
 *
 * 如果返回值小于数组大小 "datasize"，调用者
 * 可以假设在返回的消息之后没有更多的 SI 消息。
 * 否则，需要另一个调用来收集更多消息。
 *
 * 注意：这可以与其他实例的 SIGetDataEntries
 * 并行运行，因为每个实例只会修改其自身后端的 ProcState
 * 字段，且没有实例会查看其他后端的 ProcStates 字段。
 * 我们通过以共享模式获取 SInvalReadLock 来表达这一点。
 * 请注意，这与共享锁的正常（只读）解释并不完全相同！
 * 在允许出于其他原因以共享模式获取 SInvalReadLock 之前，请仔细查看交互。
 *
 * 注意：这也可以与 SIInsertDataEntries 并行运行。
 * 不能保证我们会返回在例程进入后添加的任何消息。
 *
 * 注意：我们假设 "datasize" 不会大到可能需要
 * 将对 SInvalReadLock 的保持分成多个段。
 */
int SIGetDataEntries(SharedInvalidationMessage *fc_data, int fc_datasize)
{
	SISeg	   *fc_segP;
	ProcState  *fc_stateP;
	int			fc_max;
	int			fc_n;

	fc_segP = shmInvalBuffer;
	fc_stateP = &fc_segP->procState[MyBackendId - 1];

	/*
	 * 在开始获取锁之前，快速进行一次未上锁的测试
	 * 以查看是否可能有任何内容可供读取。
	 * 在多处理器系统上，可能会出现此负载向后迁移的情况，
	 * 并在我们实际进入此函数之前发生，因此我们可能会错过
	 * 刚刚由其他处理器添加的 sinval 消息。
	 * 但是，它们不能在前一个锁获取之后向后迁移，
	 * 所以应该没问题。如果我们没有获取阻止
	 * 进一步相关失效的锁，那么任何此类情况与失效稍后到达
	 * 的情况并没有什么不同。
	 */
	if (!fc_stateP->hasMessages)
		return 0;

	LWLockAcquire(SInvalReadLock, LW_SHARED);

	
/*
	 * 我们必须在确定要读取多少消息之前重置 hasMessages。这样，如果在确定要读取的消息数量之后有新消息到达，标志会被重置，我们将在过程中注意到这些消息。
	 *
	 * 请注意，如果我们没有读取完所有消息，我们必须确保在退出之前重置这个标志！
	 */
	fc_stateP->hasMessages = false;

	/* 使用自旋锁获取 maxMsgNum 的当前值 */
	SpinLockAcquire(&fc_segP->msgnumLock);
	fc_max = fc_segP->maxMsgNum;
	SpinLockRelease(&fc_segP->msgnumLock);

	if (fc_stateP->resetState)
	{
		/*
		 * 强制重置。我们可以说我们已经处理了自重置以来添加的任何消息；这也意味着我们应该清除已通知的标志。
		 */
		fc_stateP->nextMsgNum = fc_max;
		fc_stateP->resetState = false;
		fc_stateP->signaled = false;
		LWLockRelease(SInvalReadLock);
		return -1;
	}

	/*
	 * 检索消息并推进后端的计数器，直到数据数组满了或没有更多的消息。
	 *
	 * 可能还有其他后端没有读取消息，因此我们不能在这里删除它们。SICleanupQueue() 最终会将它们从队列中移除。
	 */
	fc_n = 0;
	while (fc_n < fc_datasize && fc_stateP->nextMsgNum < fc_max)
	{
		fc_data[fc_n++] = fc_segP->buffer[fc_stateP->nextMsgNum % MAXNUMMESSAGES];
		fc_stateP->nextMsgNum++;
	}

	/*
	 * 如果我们已经完全赶上了，重置我们的“已通知”标志，这样如果我们再次落后就会再次收到信号。
	 *
	 * 如果我们还没有完全赶上，重置 hasMessages 标志，以便下次查看剩余的消息。
	 */
	if (fc_stateP->nextMsgNum >= fc_max)
		fc_stateP->signaled = false;
	else
		fc_stateP->hasMessages = true;

	LWLockRelease(SInvalReadLock);
	return fc_n;
}

/*
 * SICleanupQueue
 *		移除所有活动后端已经处理的消息
 *
 * callerHasWriteLock 为真表示调用者持有 SInvalWriteLock。
 * minFree 是要释放的最小消息槽位数。
 *
 * 此例程的可能副作用包括将一个或多个后端标记为“重置”，并向一些似乎落后太多的后端发送 PROCSIG_CATCHUP_INTERRUPT。出于文件顶部解释的原因，我们每次最多只对一个后端发送信号。
 *
 * 注意：因为在需要向其他后端发送信号时我们暂时释放写锁，因此无法保证在退出时仍有 minFree 个空闲消息槽。调用者必须重新检查并可能重试。
 */
void SICleanupQueue(bool fc_callerHasWriteLock, int fc_minFree)
{
	SISeg	   *fc_segP = shmInvalBuffer;
	int			fc_min,
				fc_minsig,
				fc_lowbound,
				fc_numMsgs,
				fc_i;
	ProcState  *fc_needSig = NULL;

	/* 锁定所有写入者和读取者 */
	if (!fc_callerHasWriteLock)
		LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);
	LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);

	/*
	 * 重新计算 minMsgNum = 所有后端的 nextMsgNum 的最小值，识别需要发送信号的最远后端（如果有）并重置任何落后太多的后端。请注意，由于我们在这里忽略 sendOnly 后端，因此即使它们是唯一活跃的后端，它们仍然可以继续发送消息而没有问题。
	 */
	fc_min = fc_segP->maxMsgNum;
	fc_minsig = fc_min - SIG_THRESHOLD;
	fc_lowbound = fc_min - MAXNUMMESSAGES + fc_minFree;

	for (fc_i = 0; fc_i < fc_segP->lastBackend; fc_i++)
	{
		ProcState  *fc_stateP = &fc_segP->procState[fc_i];
		int			fc_n = fc_stateP->nextMsgNum;

		/* 如果不活跃或已经在重置状态则忽略 */
		if (fc_stateP->procPid == 0 || fc_stateP->resetState || fc_stateP->sendOnly)
			continue;

		/*
		 * 如果我们必须释放一些空间而这个后端阻止了它，强制他进入重置状态，然后忽略他直到他赶上。
		 */
		if (fc_n < fc_lowbound)
		{
			fc_stateP->resetState = true;
			/* 向他发送信号没有意义... */
			continue;
		}

		/* 跟踪全局最小 nextMsgNum */
		if (fc_n < fc_min)
			fc_min = fc_n;

		/* 还要查看哪个未信号后端距离最远 */
		if (fc_n < fc_minsig && !fc_stateP->signaled)
		{
			fc_minsig = fc_n;
			fc_needSig = fc_stateP;
		}
	}
	fc_segP->minMsgNum = fc_min;

	/*
	 * 当 minMsgNum 变得非常大时，递减所有消息计数器以防止计数器溢出。
	 * 这种情况出现得很少，因此将其折叠到上一个循环中将是一个失败的选择。
	 */
	if (fc_min >= MSGNUMWRAPAROUND)
	{
		fc_segP->minMsgNum -= MSGNUMWRAPAROUND;
		fc_segP->maxMsgNum -= MSGNUMWRAPAROUND;
		for (fc_i = 0; fc_i < fc_segP->lastBackend; fc_i++)
		{
			/* 我们在这里不在乎跳过不活跃的条目 */
			fc_segP->procState[fc_i].nextMsgNum -= MSGNUMWRAPAROUND;
		}
	}

	/*
	 * 确定队列中仍然有多少消息，并设置我们应该重复 SICleanupQueue() 的阈值。
	 */
	fc_numMsgs = fc_segP->maxMsgNum - fc_segP->minMsgNum;
	if (fc_numMsgs < CLEANUP_MIN)
		fc_segP->nextThreshold = CLEANUP_MIN;
	else
		fc_segP->nextThreshold = (fc_numMsgs / CLEANUP_QUANTUM + 1) * CLEANUP_QUANTUM;

	/*
	 * 最后，向需要补救中断的任何人发送信号。由于
	 * SendProcSignal() 可能不够快速，我们不想在执行时持有锁。
	 */
	if (fc_needSig)
	{
		pid_t		fc_his_pid = fc_needSig->procPid;
		BackendId	fc_his_backendId = (fc_needSig - &fc_segP->procState[0]) + 1;

		fc_needSig->signaled = true;
		LWLockRelease(SInvalReadLock);
		LWLockRelease(SInvalWriteLock);
		elog(DEBUG4, "sending sinval catchup signal to PID %d", (int) fc_his_pid);
		SendProcSignal(fc_his_pid, PROCSIG_CATCHUP_INTERRUPT, fc_his_backendId);
		if (fc_callerHasWriteLock)
			LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);
	}
	else
	{
		LWLockRelease(SInvalReadLock);
		if (!fc_callerHasWriteLock)
			LWLockRelease(SInvalWriteLock);
	}
}

/*
 * SIResetAll
 *		标记所有活动的后端为 "重置"
 *
 * 当我们不知道需要使什么无效时使用此方法。这是一个
 * 全集群级别的 InvalidateSystemCaches()。这是一个仅限后端的补救措施，
 * 以避免 WAL 格式更改。
 *
 * 实现类似于 SICleanupQueue(false, MAXNUMMESSAGES + 1)，并增加了一条附加内容。
 * SICleanupQueue() 假设 minFree << MAXNUMMESSAGES，因此假设
 * hasMessages==true 对于任何重置的后端。我们甚至重置完全同步的后端，
 * 因此设置 hasMessages。
 */
void SIResetAll(void)
{
	SISeg	   *fc_segP = shmInvalBuffer;
	int			fc_i;

	LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);
	LWLockAcquire(SInvalReadLock, LW_EXCLUSIVE);

	for (fc_i = 0; fc_i < fc_segP->lastBackend; fc_i++)
	{
		ProcState  *fc_stateP = &fc_segP->procState[fc_i];

		if (fc_stateP->procPid == 0 || fc_stateP->sendOnly)
			continue;

		/* 消耗重置将更新 "nextMsgNum" 和 "signaled"。 */
		fc_stateP->resetState = true;
		fc_stateP->hasMessages = true;
	}

	fc_segP->minMsgNum = fc_segP->maxMsgNum;
	fc_segP->nextThreshold = CLEANUP_MIN;

	LWLockRelease(SInvalReadLock);
	LWLockRelease(SInvalWriteLock);
}


/*
 * GetNextLocalTransactionId --- 分配一个新的 LocalTransactionId
 *
 * 我们将 VirtualTransactionIds 拆分为两部分，以便可以在不争用共享内存的情况下
 * 分配一个新的标识符，除非在后端启动/关闭期间有一些额外开销。
 * VirtualTransactionId 的高位部分是 BackendId，低位部分是 LocalTransactionId，
 * 我们从本地计数器中分配。为了避免在短时间内重复使用 VirtualTransactionId，
 * 连续占用相同后端 ID 插槽的进程应该使用连续的本地 ID 序列，
 * 这通过复制上述的 nextLocalTransactionId 来实现。
 */
LocalTransactionId
GetNextLocalTransactionId(void)
{
	LocalTransactionId fc_result;

	/* 循环以避免在包裹时返回 InvalidLocalTransactionId */
	do
	{
		fc_result = nextLocalTransactionId++;
	} while (!LocalTransactionIdIsValid(fc_result));

	return fc_result;
}
